package org.ponly.lpf4j.impl;

import gnu.trove.TIntProcedure;
import org.ponly.common.reflect.Reflect;
import org.ponly.common.util.FileUtils;
import org.ponly.common.util.Path;
import org.ponly.common.util.ZipUtils;
import org.ponly.common.util.function.Condition;
import org.ponly.common.util.function.Function;
import org.ponly.lpf4j.PluginDescriptor;
import org.ponly.lpf4j.PluginManager;
import org.ponly.lpf4j.util.Couple;
import org.ponly.lpf4j.util.graph.CachingSemiGraph;
import org.ponly.lpf4j.util.graph.DFSTBuilder;
import org.ponly.lpf4j.util.graph.Graph;
import org.ponly.lpf4j.util.graph.GraphGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static org.ponly.lpf4j.util.Utils.*;

/**
 * Abstract plugin manager
 *
 * @author vacoor
 */
public abstract class AbstractPluginManager implements PluginManager {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractPluginManager.class);

    /* **********************************************
     *                   CONSTANT
     * ******************************************** */

    public static final String JAR_PROTOCOL = "jar";
    public static final String JAR_SEPARATOR = "!/";
    public static final String FILE_PROTOCOL = "file";
    public static final String PROPERTY_PLUGIN_PATH = "ponly.plugin.path";
    public static final String PLATFORM_PREFIX_KEY = "ponly.plugin.prefix";
    public static final String CORE_PLUGIN_ID = "org.ponly";
    public static final String META_INF = "META-INF/lpf4j";
    public static final String PLUGIN_XML = "plugin.xml";

    /* **********************************************
     *                Member Variable
     * ******************************************** */

    private boolean initialized;
    private String pluginsPath;     // plugins home
    private List<DefaultPluginDescriptor> plugins = new LinkedList<>();
    private List<DefaultPluginDescriptor> activePlugins = new CopyOnWriteArrayList<>();

    public AbstractPluginManager(String pluginsPath) {
        this.pluginsPath = pluginsPath;
    }

    /**
     * initialize plugins
     */
    @Override
    public synchronized void initialize() {
        if (initialized) {
            return;
        }

        // LOG.info("Loading bundled plugins.");

        final long start = System.currentTimeMillis();

        // loadBundledPlugins();

        LOG.info("Listing up plugins.");

        List<DefaultPluginDescriptor> archives = loadDescriptors();// TODO loadPluginArchives();

        LOG.info("Preparing plugins.");

        // inspecting plugins
        final Map<String, DefaultPluginDescriptor> inspectedDescriptorMap = new HashMap<>();
        final Map<String, DefaultPluginDescriptor> waitActiveDescriptorMap = new HashMap<>();
        for (DefaultPluginDescriptor archive : archives) {
            LOG.info("Inspecting plugin '{}'", archive.getName());

            final String pluginId = archive.getId();
            final String pluginName = archive.getName();

            // duplicate
            if (inspectedDescriptorMap.containsKey(pluginId)) {
                final PluginDescriptor inspected = inspectedDescriptorMap.get(pluginId);
                // duplicate and is new version is loaded
                if (0 < compareVersionNumbers(inspected.getVersion(), archive.getVersion())) {
                    LOG.info("Ignoring '{}' because '{}' is already loaded", pluginId, inspected.getPluginRoot());
                    continue;
                }
            }
            inspectedDescriptorMap.put(pluginId, archive);

            // compatibility / disabled
            final boolean isUserDisabled = isUserDisabled(archive);
            final boolean isIncompatible = isIncompatible(archive);
            if (isIncompatible || isUserDisabled) {
                archive.setEnabled(false);

                // TODO 加入禁用列表/失败列表

                LOG.info("Plugin '{}' can't be loaded because '{}' is {}.", pluginName, (isIncompatible ? "incompatible" : "disabled"));
            } else {
                waitActiveDescriptorMap.put(pluginId, archive);
            }
        }

        LOG.info("Checking cyclic dependencies.");

        final Graph<String> graph = createPluginIdGraph(inspectedDescriptorMap);
        final DFSTBuilder<String> builder = new DFSTBuilder<String>(graph);
        if (!builder.isAcyclic()) { // cyclic dependencies
            builder.getSCCs().forEach(new TIntProcedure() {
                private int num;

                @Override
                public boolean execute(int size) {
                    for (int j = 0; j < size; j++) {
                        // 禁用循环依赖插件
                        String id = builder.getNodeByTNumber(num + j);
                        // TODO 从激活列表中删除, 加入失败列表
                        inspectedDescriptorMap.get(id).setEnabled(false);
                        waitActiveDescriptorMap.remove(id);
                    }
                    num += size;
                    return true;
                }
            });
            final Couple<String> circularDependency = builder.getCircularDependency();
            final String id = circularDependency.getFirst();
            final String parentId = circularDependency.getSecond();

            LOG.error("Plugin should not have cyclic dependencies: {} -> {} ->...-> {}", id, parentId, id);
        }
        final List<DefaultPluginDescriptor> waitActiveDescriptors = new LinkedList<>(waitActiveDescriptorMap.values());
        final Comparator<String> idComparator = builder.comparator();
        Collections.sort(waitActiveDescriptors, new Comparator<PluginDescriptor>() {
            @Override
            public int compare(PluginDescriptor o1, PluginDescriptor o2) {
                final String pluginId1 = o1.getId();
                final String pluginId2 = o2.getId();
                if (CORE_PLUGIN_ID.equals(pluginId1)) return -1;
                if (CORE_PLUGIN_ID.equals(pluginId2)) return 1;
                return idComparator.compare(pluginId1, pluginId2);
            }
        });

        // check dependencies and initializing
        for (final Iterator<DefaultPluginDescriptor> it = waitActiveDescriptors.iterator(); it.hasNext(); ) {
            final DefaultPluginDescriptor descriptor = it.next();
            final String pluginId = descriptor.getId();
            final String pluginName = descriptor.getName();

            checkDependants(descriptor, new Condition<String>() {
                @Override
                public boolean value(final String dependencyPluginId) {
                    if (!waitActiveDescriptorMap.containsKey(dependencyPluginId)) {
                        descriptor.setEnabled(false);
                        // TODO 1. 加入失败列表 descriptor.id
                        // TODO 2. 加入 miss dependency: dependency plugin id
                        String depName = dependencyPluginId;
                        if (inspectedDescriptorMap.containsKey(dependencyPluginId)) {
                            depName = inspectedDescriptorMap.get(dependencyPluginId).getName();
                        }

                        // * 提示依赖没有安装或被禁用
                        if (isUserDisabled(dependencyPluginId)) {
                            LOG.warn("Plugin '{}' required '{}' plugin is disabled.", pluginName, depName);
                        } else {
                            LOG.warn("Plugin '{}' required '{}' plugin is not installed.", pluginName, depName);
                        }

                        waitActiveDescriptorMap.remove(pluginId);
                        it.remove();
                        return false;
                    }
                    return true;
                }
            }, new Function<String, PluginDescriptor>() {
                @Override
                public PluginDescriptor apply(String pluginId) {
                    return waitActiveDescriptorMap.get(pluginId);
                }
            }, new HashSet<String>());
        }

        // sort and initializing
        final Comparator<String> comparator = new DFSTBuilder<String>(createPluginIdGraph(waitActiveDescriptorMap)).comparator();
        Collections.sort(waitActiveDescriptors, new Comparator<DefaultPluginDescriptor>() {
            @Override
            public int compare(DefaultPluginDescriptor o1, DefaultPluginDescriptor o2) {
                return comparator.compare(o1.getId(), o2.getId());
            }
        });

        plugins.addAll(inspectedDescriptorMap.values());

        int i = 0;
        ClassLoader parentClassLoader = getCoreClassLoader();
        for (DefaultPluginDescriptor descriptor : waitActiveDescriptors) {
            LOG.info("Plugin initializing: '{}({})', {}/{}", descriptor.getName(), descriptor.getId(), ++i, waitActiveDescriptors.size());
            try {
                initPluginClassLoader(descriptor, parentClassLoader, new Function<String, PluginDescriptor>() {
                    @Override
                    public PluginDescriptor apply(String pluginId) {
                        return waitActiveDescriptorMap.get(pluginId);
                    }
                });
                initializePlugin(descriptor);
                activePlugins.add(descriptor);
            } catch (Throwable t) {
                // TODO 加入失败列表
                LOG.error("Plugin can't initialized: '{}', {}/{}", descriptor.getName(), i, waitActiveDescriptors.size());
            }
        }

        initialized = true;
        LOG.info(plugins.size() + " plugins initialized in " + (System.currentTimeMillis() - start) + " ms");
        logPlugins();
        clearJarUrlCache();
    }

    /**
     * destroy all initialized plugins and clear all plugins
     */
    @Override
    public synchronized void destroy() {
        for (DefaultPluginDescriptor descriptor : activePlugins) {
            destroyPlugin(descriptor, true);
        }
        activePlugins.clear();
        plugins.clear();
        initialized = false;
    }

    /**
     * initialize plugin
     *
     * @param descriptor plugin descriptor
     */
    protected void initializePlugin(PluginDescriptor descriptor) {
        doInitializePlugin(descriptor);
    }

    /**
     * instantiate given plugin instance and initialize
     *
     * @param descriptor plugin descriptor
     */
    protected abstract void doInitializePlugin(PluginDescriptor descriptor);

    /**
     * destroy a given active plugin and release plugin's class loader
     *
     * @param descriptor         plugin descriptor
     * @param releaseClassLoader release plugin class loader
     */
    protected void destroyPlugin(PluginDescriptor descriptor, boolean releaseClassLoader) {
        final ClassLoader pluginClassLoader = descriptor.getPluginClassLoader();

        doDestroyPlugin(descriptor);

        /*-
         * release class loader resources
         * https://docs.oracle.com/javase/7/docs/technotes/guides/net/ClassLoader.html
         */
        if (releaseClassLoader) {
            releasePluginClassLoader(pluginClassLoader);
        }
    }

    /**
     * destroy a given active plugin
     *
     * @param descriptor plugin descriptor
     */
    protected abstract void doDestroyPlugin(PluginDescriptor descriptor);

    /**
     * 检查依赖关系
     *
     * @param descriptor     插件描述符
     * @param toDescriptorFn plugin id - 描述符转换函数
     * @param checker        依赖插件检查条件
     * @param processed      已经处理的依赖清单
     * @return 依赖是否都有效
     */
    private boolean checkDependants(final PluginDescriptor descriptor,
                                    final Condition<String> checker,
                                    final Function<String, PluginDescriptor> toDescriptorFn,
                                    final Set<String> processed) {
        processed.add(descriptor.getId());

        final String[] dependentPluginIds = descriptor.getDependentPluginIds();
        final Set<String> optionalDependencies = new HashSet<String>(Arrays.asList(descriptor.getOptionalDependentPluginIds()));
        for (final String dependentPluginId : dependentPluginIds) {
            // 如果不是可选依赖
            if (!processed.contains(dependentPluginId) && !optionalDependencies.contains(dependentPluginId)) {
                if (!checker.value(dependentPluginId)) {
                    return false;
                }
                // 存在依赖并且依赖无效
                final PluginDescriptor dependantPluginDescriptor = toDescriptorFn.apply(dependentPluginId);
                if (null != dependantPluginDescriptor && !checkDependants(dependantPluginDescriptor, checker, toDescriptorFn, processed)) {
                    return false;
                }
            }
        }
        return true;
    }

    protected boolean isIncompatible(PluginDescriptor descriptor) {
        // TODO
        return false;
    }

    protected boolean isUserDisabled(PluginDescriptor descriptor) {
        return null != descriptor && isUserDisabled(descriptor.getId());
    }

    protected abstract boolean isUserDisabled(String pluginId);

    protected abstract Collection<PluginDescriptor> loadBundledPlugins();


    protected ClassLoader getCoreClassLoader() {
        return getClass().getClassLoader();
    }

    /**
     * initialize plugin class loader for descriptor
     *
     * @param descriptor   plugin descriptor
     * @param parentLoader parent class loader
     */
    protected void initPluginClassLoader(DefaultPluginDescriptor descriptor,
                                         ClassLoader parentLoader,
                                         Function<String, PluginDescriptor> toDescriptorFn) {
        if (CORE_PLUGIN_ID.equals(descriptor.getId()) || descriptor.isUseCoreClassLoader()) {
            descriptor.setLoader(parentLoader);
        } else {
            final String[] dependentPluginIds = descriptor.getDependentPluginIds();
            final ClassLoader[] parentLoaders = getParentLoaders(dependentPluginIds, toDescriptorFn);

            // create dependencies class loader
            descriptor.setLoader(createPluginClassLoader(
                    descriptor,
                    0 < parentLoaders.length ? parentLoaders : new ClassLoader[]{parentLoader}
            ));
        }

        LOG.debug("Plugin descriptor progress for '{}'", descriptor.getName());
    }

    /**
     * 获取给定插件列表的类加载器
     *
     * @param pluginIds 插件 id 列表
     * @return 对应的类加载器
     */
    private ClassLoader[] getParentLoaders(String[] pluginIds, Function<String, PluginDescriptor> toDescriptorFn) {
        final List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
        for (final String id : pluginIds) {
            PluginDescriptor pluginDescriptor = toDescriptorFn.apply(id);
            if (null == pluginDescriptor) {  // many be optional dependency
                continue;
            }
            final ClassLoader loader = pluginDescriptor.getPluginClassLoader();
            if (null == loader) {
                LOG.error("Plugin class loader should be initialized for plugin " + id);
            }
            classLoaders.add(loader);
        }
        return classLoaders.toArray(new ClassLoader[classLoaders.size()]);
    }

    /**
     * 创建插件类加载器
     *
     * @param descriptor    插件描述符
     * @param parentLoaders 父类加载器
     * @return 插件类加载器
     */
    protected ClassLoader createPluginClassLoader(DefaultPluginDescriptor descriptor, ClassLoader[] parentLoaders) {
        final List<File> classPath = descriptor.getClassPath();
        final String pluginId = descriptor.getId();
        final File pluginRoot = descriptor.getPluginRoot();

        releasePluginClassLoader(descriptor.getPluginClassLoader());
        try {
            final List<URL> urls = new ArrayList<URL>(classPath.size());
            for (File aClassPath : classPath) {
                // it is critical not to have "." and ".." in classpath elements
                urls.add(aClassPath.getCanonicalFile().toURI().toURL());
            }
            return new PluginClassLoader(urls, parentLoaders, pluginId, descriptor.getVersion(), pluginRoot);
        } catch (IOException e) {
            LOG.warn("Plugin {} create class loader failed: {}", descriptor.getName(), e);
        }
        return null;
    }

    protected void releasePluginClassLoader(ClassLoader pluginClassLoader) {
        final ClassLoader coreClassLoader = getCoreClassLoader();
        if (null == pluginClassLoader || pluginClassLoader.equals(coreClassLoader)) {
            return;
        }
        try {
            if (pluginClassLoader instanceof Closeable) {   // from jdk 1.7
                ((Closeable) pluginClassLoader).close();
            } else {
                Reflect loader = Reflect.from(pluginClassLoader);
                try {
                    loader.field("ucp").call("closeLoaders");
                } catch (Exception ignore) {
                    // ignore
                }
                try {
                    loader.call("close");
                } catch (Exception ignore) {
                    // ignore
                }
            }
        } catch (Exception ex) {
            LOG.warn("Shutdown class loader fail.", ex);
        }
    }

    /**
     * 创建一个插件引用图
     *
     * @param idToDescriptorMap 所有插件的 ID - 插件描述符 Map
     * @return 插件引用图
     */
    private Graph<String> createPluginIdGraph(final Map<String, ? extends PluginDescriptor> idToDescriptorMap) {
        final List<String> ids = new ArrayList<String>(idToDescriptorMap.keySet());

        // this magic ensures that the dependent plugins always follow their dependencies in lexicographic order
        // needed to make sure that extensions are always in the same order
        Collections.sort(ids, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.compareTo(o1);
            }
        });
        return GraphGenerator.create(CachingSemiGraph.create(new GraphGenerator.SemiGraph<String>() {
            @Override
            public Collection<String> getNodes() {
                return ids;
            }

            @Override
            public Iterator<String> getIn(String pluginId) {
                final PluginDescriptor descriptor = idToDescriptorMap.get(pluginId);
                List<String> plugins = new ArrayList<String>();
                for (String dependentPluginId : descriptor.getDependentPluginIds()) {
                    // check for missing optional dependency
                    PluginDescriptor dep = idToDescriptorMap.get(dependentPluginId);
                    if (null != dep) {
                        //if 'dep' refers to a module we need to add the real plugin containing this module only if it's still enabled, otherwise the graph will be inconsistent
                        String realPluginId = dep.getId();
                        if (idToDescriptorMap.containsKey(realPluginId)) {
                            plugins.add(realPluginId);
                        }
                    }
                }
                return plugins.iterator();
            }
        }));
    }


    /* *******************************************************
     *                  active plugin
     * ***************************************************** */

    public boolean activePlugin(String pluginId) {
        DefaultPluginDescriptor descriptor = findPlugin(pluginId);
        return (null != descriptor && activePlugin(descriptor, true));
    }

    protected boolean activePlugin(DefaultPluginDescriptor descriptor, boolean mustUserDisabled) {
        boolean enabled = doEnablePlugin(descriptor, mustUserDisabled);
        descriptor.setEnabled(enabled);
        return enabled;
    }

    protected boolean doEnablePlugin(final DefaultPluginDescriptor descriptor, final boolean mustUserDisabled) {
        final String pluginId = descriptor.getId();
        final String pluginName = descriptor.getName();
        // active or !user disabled
        if (isActive(pluginId) || (mustUserDisabled && !isUserDisabled(descriptor))) {
            return false;
        }

        final boolean isIncompatible = isIncompatible(descriptor);
        final boolean isUserDisabled = isUserDisabled(descriptor);
        final ClassLoader parentClassLoader = getCoreClassLoader();

        if (isIncompatible || isUserDisabled) {
            // incompatible or user disabled
            descriptor.setEnabled(false);
            descriptor.setLoader(createPluginClassLoader(descriptor, new ClassLoader[]{parentClassLoader}));

            LOG.info("Plugin '{}' can't be loaded because '{}' is {}.", pluginName, (isIncompatible ? "incompatible" : "disabled"));
            return false;
        }

        final Map<String, DefaultPluginDescriptor> activeDescriptorMap = asMap(activePlugins);
        final boolean checked = checkDependants(descriptor, new Condition<String>() {
            @Override
            public boolean value(String dependencyPluginId) {
                if (!activeDescriptorMap.containsKey(dependencyPluginId)) {
                    descriptor.setEnabled(false);
                    // TODO 1. 加入失败列表 descriptor.id
                    // TODO 2. 加入 miss dependency: dependency plugin id
                    String depName = dependencyPluginId;

                    // * 提示依赖没有安装或被禁用
                    if (isUserDisabled(dependencyPluginId)) {
                        LOG.error("Plugin '{}' required '{}' plugin is disabled.", pluginName, depName);
                    } else {
                        LOG.error("Plugin '{}' required '{}' plugin is not installed.", pluginName, depName);
                    }
                    return false;
                }
                return true;
            }
        }, new Function<String, PluginDescriptor>() {
            @Override
            public PluginDescriptor apply(String pluginId) {
                return activeDescriptorMap.get(pluginId);
            }
        }, activeDescriptorMap.keySet());

        //
        descriptor.setEnabled(checked);

        if (!checked) {
            LOG.warn("Plugin '{}' initialized error: dependencies is not installed/disabled.", descriptor.getName());
            return false;
        }

        initPluginClassLoader(descriptor, parentClassLoader, new Function<String, PluginDescriptor>() {
            @Override
            public PluginDescriptor apply(String pluginId) {
                return activeDescriptorMap.get(pluginId);
            }
        });
        initializePlugin(descriptor);

        activePlugins.add(descriptor);

        return true;
    }


    /* *******************************************************
     *                  inactive plugin
     * ***************************************************** */

    public boolean disablePlugin(String pluginId) {
        DefaultPluginDescriptor descriptor = findPlugin(pluginId);
        return (null != descriptor && disablePlugin(descriptor));
    }

    @SuppressWarnings({"unchecked"})
    protected boolean disablePlugin(DefaultPluginDescriptor descriptor) {
        boolean disabled = doDisablePlugin(descriptor, Condition.TRUE);
        descriptor.setEnabled(!disabled);
        return true;
    }

    protected boolean doDisablePlugin(DefaultPluginDescriptor descriptor, final Condition<String> checker) {
        final String pluginId = descriptor.getId();
        if (isUserDisabled(pluginId)) {
            return false;
        }
        if (isActive(pluginId)) {
            final Map<String, DefaultPluginDescriptor> activePluginMap = asMap(activePlugins);
            return processDependents(descriptor.getId(), createPluginIdGraph(activePluginMap), new Condition<String>() {
                @Override
                public boolean value(String pluginId) {
                    final DefaultPluginDescriptor descriptor = activePluginMap.get(pluginId);
                    if (null != descriptor && checker.value(pluginId)) {
                        // destroy plugin
                        destroyPlugin(descriptor, true);
                        return true;
                    }
                    return false;
                }
            });
        }
        return checker.value(pluginId);
    }

    protected boolean processDependents(String pluginId, Graph<String> graph, Condition<String> checker) {
        for (Iterator<String> out = graph.getOut(pluginId); out.hasNext(); ) {
            String next = out.next();
            // last is self
            if (!next.equals(pluginId) && !processDependents(next, graph, checker)) {
                return false;
            }
        }
        return checker.value(pluginId);
    }

    /* *********************************************************
     *                       install
     * ******************************************************* */

    public boolean install(final File pluginFile) throws IOException {
        LOG.info("Plugin installing: {}", pluginFile);

        if (!pluginFile.exists()) {
            LOG.error("Plugin can't installed because plugin file is not exists: '{}'", pluginFile);

            throw new FileNotFoundException(pluginFile.getPath());
        }

        File pluginRoot = pluginFile;
        boolean deleteRoot = false;

        // should extract to temporary path (contains lib or classes)
        if (pluginFile.isFile() && isUnarchiveFile(pluginFile)) {
            String name = FileUtils.getNameWithoutExtension(pluginFile.getName());
            File path = new File(System.getProperty("java.io.tmpdir"), ".lpf4j");
            File tmpRoot = new File(path, name);
            ZipUtils.extract(pluginFile, tmpRoot.getAbsolutePath(), Charset.forName("UTF-8"));
            pluginRoot = tmpRoot;
            deleteRoot = true;
        }

        try {
            // invalid file
            if (!pluginRoot.isDirectory() && !isJarOrZip(pluginRoot)) {
                LOG.error("Plugin can't installed because not found plugin descriptor in {}", pluginFile);
                throw new IllegalArgumentException("invalid plugin file.");
            }

            DefaultPluginDescriptor pluginDescriptor = loadDescriptor(pluginRoot, PLUGIN_XML);
            if (null == pluginDescriptor) {
                LOG.error("Plugin can't installed because not found plugin descriptor in {}", pluginFile);

                throw new IllegalStateException("Plugin descriptor not found in plugin dir/bundle.");
            }

            String pluginId = pluginDescriptor.getId();
            DefaultPluginDescriptor oldDescriptor = findPlugin(pluginId);

            // upgrade plugin (installed)
            if (null != oldDescriptor) {
                File oldPluginRoot = oldDescriptor.getPluginRoot();
                if (pluginRoot.equals(oldPluginRoot)) {
                    // 已经是安装状态
                    LOG.warn("Plugin '{}' already installed.", pluginDescriptor.getName());
                    return false;
                }

                return doUpgrade(oldDescriptor, pluginDescriptor, pluginRoot);
            }

            return doInstall(pluginDescriptor, pluginRoot);
        } finally {
            if (deleteRoot) {
                delete(pluginRoot);
            }
            clearJarUrlCache();
        }
    }

    protected boolean doInstall(DefaultPluginDescriptor descriptor, final File pluginRoot) throws IOException {
        final String pluginId = descriptor.getId();
        final String pluginName = pluginRoot.getName();

        File target = makeUsablePluginDirectory(pluginName);
        if (pluginRoot.isFile()) {  // normal jar/zip
            FileUtils.copy(pluginRoot, target);
        } else {                    // plugin directory
            FileUtils.copyDirectory(pluginRoot, target);
        }

        descriptor = loadDescriptor(target, PLUGIN_XML);
        if (null == descriptor) {
            throw new IllegalStateException();
        }

        plugins.add(descriptor);

        LOG.info("Plugin '{}' installed.", descriptor.getName());

        // TODO 考虑安装完毕后应该是禁用，手动去启用还是默认启用
        return doEnablePlugin(descriptor, false);
    }

    protected boolean doUpgrade(DefaultPluginDescriptor oldDescriptor, DefaultPluginDescriptor newDescriptor, File pluginRoot) throws IOException {
        final String pluginId = oldDescriptor.getId();
        final List<String> upgradeList = new LinkedList<>();

        // destroy and save associated plugin
        boolean destroyed = doDisablePlugin(oldDescriptor, new Condition<String>() {
            @Override
            public boolean value(String pluginId) {
                if (!isActive(pluginId)) {
                    return false;
                }
                if (!upgradeList.contains(pluginId)) {
                    upgradeList.add(pluginId);
                }
                return true;
            }
        });
        if (!destroyed) {
            return false;
        }

        plugins.remove(oldDescriptor);

        File path = oldDescriptor.getPluginRoot();
        delete(path);

        // fixed 1.1 --> 1.2 dir has same name
        path = makeUsablePluginDirectory(pluginRoot.getName());
        if (pluginRoot.isFile()) {
            FileUtils.copy(pluginRoot, path);
        } else {
            FileUtils.copyDirectory(pluginRoot, path);
        }

        newDescriptor = loadDescriptor(path, PLUGIN_XML);
        if (null == newDescriptor) {
            // TODO LOG
            return false;
        }

        plugins.add(newDescriptor);

        LOG.info("Plugin '{}' upgraded.", newDescriptor.getName());

        boolean startup = false;
        for (String pid : upgradeList) {
            DefaultPluginDescriptor d = findPlugin(pid);
            if (null != d && doEnablePlugin(d, false) && pid.equals(pluginId)) {
                startup = true;
            }
        }
        return startup;
    }

    private void delete(File f) {
        if (f.exists()) {
            try {
                FileUtils.delete(f, true, true);
            } catch (IOException e) {
                f.deleteOnExit();
            }
        }
    }

    private File makeUsablePluginDirectory(String filename) {
        File file = new File(pluginsPath, filename);
        try {
            FileUtils.ensureDirectory(file.getParentFile());
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        if (!file.exists()) {
            return file;
        }
        final String prefix = FileUtils.getNameWithoutExtension(filename);
        final String suffix = FileUtils.getExtension(filename, true);
        for (int i = 0; file.exists(); i++) {
            file = new File(pluginsPath, prefix + i + suffix);
        }
        return file;
    }


    /* *********************************************************
     *                       uninstall
     * ******************************************************* */

    /**
     * 卸载给定插件
     *
     * @param pluginId 插件ID
     * @return 是否卸载成功
     * @throws IOException
     */
    public boolean uninstall(final String pluginId) throws IOException {
        try {
            DefaultPluginDescriptor descriptor = findPlugin(pluginId);
            if (null == descriptor) {
                return false;
            }

            File path = descriptor.getPluginRoot();
            if (!Path.get(path.getAbsolutePath()).startsWith(Path.get(pluginsPath))) {
                // TODO
                throw new IllegalStateException("预安装的插件不允许卸载");
            }

            plugins.remove(descriptor);
            boolean destroyed = doDisablePlugin(descriptor, new Condition<String>() {
                @Override
                public boolean value(String pid) {
                    // TODO add to unresolved plugins
                    // 删除后把所有依赖自己的添加到无法解析列表中
                    if (!pluginId.equals(pid)) {
//                        unresolvedPluginIds.add(pluginId.getIdString());
//                        invalidDependencies.add(pid.getIdString());
                    }
                    return true;
                }
            });
            delete(path);
            return destroyed;
        } finally {
            clearJarUrlCache();
        }
    }


    protected boolean isActive(String pluginId) {
        return null != findActivePlugin(pluginId);
    }

    protected DefaultPluginDescriptor findActivePlugin(String pluginId) {
        return findPlugin(activePlugins, pluginId);
    }

    protected DefaultPluginDescriptor findPlugin(String pluginId) {
        return findPlugin(plugins, pluginId);
    }

    private DefaultPluginDescriptor findPlugin(Iterable<DefaultPluginDescriptor> descriptors, String pluginId) {
        DefaultPluginDescriptor descriptor = null;
        for (DefaultPluginDescriptor d : descriptors) {
            if (d.getId().equals(pluginId)) {
                descriptor = d;
                break;
            }
        }
        return descriptor;
    }

    private Map<String, DefaultPluginDescriptor> asMap(Iterable<DefaultPluginDescriptor> descriptors) {
        Map<String, DefaultPluginDescriptor> map = new HashMap<String, DefaultPluginDescriptor>();
        for (DefaultPluginDescriptor descriptor : descriptors) {
            map.put(descriptor.getId(), descriptor);
        }
        return map;
    }

    private void logPlugins() {
        List<String> loadedBundled = new ArrayList<String>();
        List<String> disabled = new ArrayList<String>();
        List<String> loadedCustom = new ArrayList<String>();

        for (PluginDescriptor descriptor : plugins) {
            final String version = descriptor.getVersion();
            String s = descriptor.getName() + (null != version ? " (" + version + ")" : "");
            if (descriptor.isEnabled()) {
                if (descriptor.isBundled()) {
                    loadedBundled.add(s);
                } else {
                    loadedCustom.add(s);
                }
            } else {
                disabled.add(s);
            }
        }

        Collections.sort(loadedBundled);
        Collections.sort(loadedCustom);
        Collections.sort(disabled);

        LOG.info(" ");
        LOG.info("###############################################");
        LOG.info(" ");
        LOG.info(" {} VERSION {}", SYSTEM_NAME, SYSTEM_VERSION);
        LOG.info(" ");
        LOG.info(" Loaded bundled plugins: " + join(loadedBundled, ", "));
        if (!loadedCustom.isEmpty()) {
            LOG.info(" Loaded custom plugins: " + join(loadedCustom, ", "));
        }
        if (!disabled.isEmpty()) {
            LOG.info(" Disabled plugins: " + join(disabled, ", "));
        }
        LOG.info(" ");
        LOG.info("###############################################");
        LOG.info(" ");
    }

    /* ***********************************************************
     *             PLUGIN SCAN HELPER / 插件扫描辅助方法
     * ********************************************************* */

    /**
     * 加载插件目录
     *
     * @param file     插件目录
     * @param filename 插件描述文件
     * @return 插件描述符或null
     */
    protected DefaultPluginDescriptor loadDescriptorFromDir(File file, String filename) {
        File descriptorFile = new File(file, META_INF + File.separator + filename);
        if (descriptorFile.exists()) {
            LOG.debug("Found plugin descriptor: {}", descriptorFile);

            try {
                DefaultPluginDescriptor descriptor = new DefaultPluginDescriptor(file);
                descriptor.readExternal(descriptorFile.toURI().toURL());
                return descriptor;
            } catch (Throwable ex) {
                LOG.warn("Cannot load {}: {}", file, ex);
            }
        }
        return null;
    }


    /**
     * 加载 jar 插件
     *
     * @param file     插件jar
     * @param filename 插件描述符文件
     * @return 插件描述符或null
     */
    protected DefaultPluginDescriptor loadDescriptorFromJar(File file, String filename) {
        try {
            final String fileURL = file.toURI().toASCIIString().replace("!", "%21");
            final URL jarURL = new URL(JAR_PROTOCOL + ":" + fileURL + JAR_SEPARATOR + META_INF + '/' + filename);

            ZipFile zipFile = new ZipFile(file);
            try {
                ZipEntry entry = zipFile.getEntry(META_INF + '/' + filename);
                if (null != entry) {
                    LOG.debug("Found plugin descriptor: '{}'", jarURL);

                    DefaultPluginDescriptor descriptor = new DefaultPluginDescriptor(file);
                    descriptor.readExternal(zipFile.getInputStream(entry));
                    return descriptor;
                }
            } catch (Throwable e) {
                LOG.warn("File '{}' contains invalid plugin descriptor.", file.getName(), e);
            } finally {
                zipFile.close();
            }
        } catch (Throwable e) {
            LOG.error("Cannot load '{}'", file, e);
        }
        return null;
    }

    /**
     * 加载插件目录或jar
     *
     * @param file     插件目录或 jar 文件
     * @param filename 插件描述符文件
     * @return 插件描述符或null
     */
    public DefaultPluginDescriptor loadDescriptor(final File file, final String filename) {
        DefaultPluginDescriptor descriptor = null;
        boolean directory = file.isDirectory();
        if (directory) {
            /*-
             * 如果是目录
             * 1. 首先尝试直接作为插件目录加载
             * 2. 可能是有依赖的形式: 插件在插件目录的 lib 目录下, 尝试加载 plugin_dir/lib/plugin.jar(plugin_dir)
             */
            descriptor = loadDescriptorFromDir(file, filename);
            if (null == descriptor) {
                // 如果插件目录为空, 尝试从 lib 下加载
                File libDir = new File(file, "lib");
                if (!libDir.isDirectory()) {
                    return null;
                }
                final File[] files = libDir.listFiles();
                if (null == files || 0 == files.length) {
                    return null;
                }

                /*-
                 * 排序 lib 下所有文件/目录, 所有名称以插件名称开头的前置, resources 目录后置
                 */
                Arrays.sort(files, new Comparator<File>() {
                    @Override
                    public int compare(File o1, File o2) {
                        if (o2.getName().startsWith(file.getName())) return Integer.MAX_VALUE;
                        if (o1.getName().startsWith(file.getName())) return -Integer.MAX_VALUE;
                        if (o2.getName().startsWith("resources")) return -Integer.MAX_VALUE;
                        if (o1.getName().startsWith("resources")) return Integer.MAX_VALUE;
                        return 0;
                    }
                });

                // 在子目录中查找插件
                for (final File child : files) {
                    if (isJarOrZip(child)) {
                        // 如果是打包, 查找后跳出
                        descriptor = loadDescriptorFromJar(child, filename);
                        if (null != descriptor) {
                            descriptor.setPath(file);
                            break;
                        }
                    } else if (child.isDirectory()) {
                        DefaultPluginDescriptor descriptor1 = loadDescriptorFromDir(child, filename);

                        // 如果是目录插件, 不允许有多个插件描述符
                        if (null != descriptor1) {
                            if (null != descriptor) {
                                LOG.error("Cannot load '{}' because two or more {}'s detected", file, filename);
                                return null;
                            }
                            descriptor = descriptor1;
                            descriptor.setPath(file);
                        }
                    }
                }
            }
        } else if (isJarOrZip(file) && file.exists()) {
            descriptor = loadDescriptorFromJar(file, filename);
        }
        return descriptor;
    }

    /**
     * 加载插件目录中所有插件
     *
     * @param pluginsHome  插件目录
     * @param result       扫描结果
     * @param pluginsCount 总计插件数量
     */
    public void loadDescriptors(File pluginsHome,
                                List<DefaultPluginDescriptor> result,
                                int pluginsCount) {
        final File[] files = pluginsHome.listFiles();
        if (null != files) {
            int i = result.size();
            for (File file : files) {
                final DefaultPluginDescriptor descriptor = loadDescriptor(file, PLUGIN_XML);
                if (null == descriptor) {
                    continue;
                }

                LOG.debug("Plugin progress '{}', {}/{}", descriptor.getName(), ++i, pluginsCount);

                int oldIndex = result.indexOf(descriptor);
                if (oldIndex >= 0) {
                    final DefaultPluginDescriptor oldDescriptor = result.get(oldIndex);
                    // 存在多个版本使用最新版本
                    if (compareVersionNumbers(oldDescriptor.getVersion(), descriptor.getVersion()) < 0) {
                        result.set(oldIndex, descriptor);
                    }
                } else {
                    result.add(descriptor);
                }
            }
        }
    }

    /**
     * 从 classpath 中扫描插件并添加到结果中
     *
     * @param result 结果集
     * @param loader ClassPath 类加载器
     */
    private void loadDescriptorsFromClassPath(List<DefaultPluginDescriptor> result, ClassLoader loader) {
        Collection<URL> urls = new HashSet<URL>();

        // 获取平台插件
        String platformPrefix = System.getProperty(PLATFORM_PREFIX_KEY);
        if (null != platformPrefix) {
            URL resource = loader.getResource(META_INF + '/' + platformPrefix + "Plugin.xml");
            if (null != resource) {
                urls.add(resource);
            }
        }

        // 获取常规插件
        try {
            Enumeration<URL> enumeration = loader.getResources(META_INF + '/' + PLUGIN_XML);
            while (enumeration.hasMoreElements()) {
                urls.add(enumeration.nextElement());
            }
        } catch (IOException e) {
            LOG.info("加载 {}/{} 失败: {}", META_INF, PLUGIN_XML, e);
            return;
        }

        int i = 0;
        for (URL url : urls) {
            DefaultPluginDescriptor descriptor = loadDescriptorFromResource(url);
            if (null != descriptor) {
                if (!result.contains(descriptor)) {
                    descriptor.setUseCoreClassLoader(true);                 // classpath 中都使用核心类加载器
                    descriptor.setBundled(true);
                    result.add(descriptor);

                    LOG.debug("Plugin descriptor loaded for '{}', progress: {}/{}", descriptor.getName(), ++i, urls.size());
                } else {
                    LOG.info("Plugin descriptor '{}' already exists, ignore ('{}')", descriptor.getName(), url);
                }
            }
        }
    }

    /**
     * 加载给定的插件描述符
     *
     * @param resource 资源描述符文件 url (不是插件)
     * @return 资源描述符
     */
    private DefaultPluginDescriptor loadDescriptorFromResource(URL resource) {
        try {
            if (FILE_PROTOCOL.equals(resource.getProtocol())) {
                File descriptorFile = urlToFile(resource);
                File pluginDir = descriptorFile.getParentFile().getParentFile().getParentFile();
                return loadDescriptor(pluginDir, descriptorFile.getName());
            } else if (JAR_PROTOCOL.equals(resource.getProtocol())) {
                String path = resource.getFile();
                File pluginJar = urlToFile(new URL(path.substring(0, path.indexOf(JAR_SEPARATOR))));
                return loadDescriptor(pluginJar, getFileName(path));
            }
        } catch (Throwable e) {
            LOG.info("Cannot load {}: {}", resource, e);
        }

        return null;
    }

    // work around corrupted URLs produced by File.getURL()
    protected static File urlToFile(URL url) throws URISyntaxException, MalformedURLException {
        try {
            return new File(url.toURI());
        } catch (URISyntaxException e) {
            String str = url.toString();
            if (str.indexOf(' ') > 0) {
                return new File(new URL(str.replace(" ", "%20")).toURI());
            }
            throw e;
        }
    }

    /**
     * 从系统属性指定的插件路径中扫描插件并添加到结果中
     *
     * @param result 插件结果集
     */
    private void loadDescriptorsFromProperty(List<DefaultPluginDescriptor> result) {
        final String pluginPaths = System.getProperty(PROPERTY_PLUGIN_PATH);
        if (null == pluginPaths) {
            return;
        }

        for (StringTokenizer t = new StringTokenizer(pluginPaths, File.pathSeparator + ","); t.hasMoreTokens(); ) {
            String pluginDirOrJar = t.nextToken();
            final DefaultPluginDescriptor ideaPluginDescriptor = loadDescriptor(new File(pluginDirOrJar), PLUGIN_XML);
            if (null != ideaPluginDescriptor) {
                result.add(ideaPluginDescriptor);
            }
        }
    }

    /**
     * 加载所有的插件描述符
     * <p>
     * 该方法将从以下位置加载插件描述符:<br/>
     * 1. 用户插件目录 <br />
     * 2. 预安装插件目录 <br />
     * 3. 系统属性指定的插件列表 <br />
     * 4. ClassPath <br />
     *
     * @return 可以加载的所有插件描述符
     */
    public List<DefaultPluginDescriptor> loadDescriptors() {
        List<DefaultPluginDescriptor> result = new LinkedList<DefaultPluginDescriptor>();

        int pluginsCount = countPlugins(pluginsPath);

        /**
         * 加载优先级: (当存在相同id插件,前者覆盖后者)
         * 1. 用户安装插件
         * 2. 预安装插件
         * 3. 系统属性指定插件
         * 4. classpath 插件
         */
        loadDescriptors(new File(pluginsPath), result, pluginsCount);

        loadDescriptorsFromProperty(result);

        loadDescriptorsFromClassPath(result, getCoreClassLoader());

        return result;
    }

    private int countPlugins(String pluginsPath) {
        File configuredPluginsDir = new File(pluginsPath);
        if (configuredPluginsDir.exists()) {
            String[] list = configuredPluginsDir.list();
            if (null != list) {
                return list.length;
            }
        }
        return 0;
    }

    @Override
    public PluginDescriptor[] getPlugins() {
        return plugins.toArray(new PluginDescriptor[plugins.size()]);
    }

    @Override
    public PluginDescriptor[] getActivePlugins() {
        return activePlugins.toArray(new PluginDescriptor[activePlugins.size()]);
    }

    @Override
    public PluginDescriptor[] getUnresolvedPlugins() {
        // TODO
        return new PluginDescriptor[0];
    }

    @Override
    public PluginDescriptor getPlugin(String pluginId) {
        return findPlugin(pluginId);
    }

    @Override
    public boolean enablePlugin(String pluginId) {
        return activePlugin(pluginId);
    }

    @Override
    public boolean installPlugin(File pluginFile) throws IOException {
        return install(pluginFile);
    }

    @Override
    public boolean uninstallPlugin(String pluginId) throws IOException {
        return uninstall(pluginId);
    }
}
